﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.IdentityModel.Claims;
using Sinacor.Infra.Service.Security.Session;
using System.ServiceModel.Channels;
using Sinacor.Infra.Common.Security.Authentication;
using System.Threading;
using Sinacor.Infra.Common.Security.Authentication.Credentials;
using System.Security.Cryptography;
using Sinacor.Infra.Core.Licensing;
using Sinacor.Infra.Common.Exceptions;
using PSE.Framework.Configuration;

namespace Sinacor.Infra.Service.Security.Authorization
{
    public class SinacorAuthorizationManager : ServiceAuthorizationManager
    {
        /// <summary>
        /// Mutex responsavel por efetuar o lock na chamada do serviço de licença
        /// </summary>
        private static Mutex _mutex = new Mutex();

        /// <summary>
        /// Efetua a verificação de acesso à operação.
        /// </summary>
        /// <param name="operationContext">contexto de operação</param>
        /// <returns>true se o acesso foi permitido, caso contrário, false.</returns>
        protected override bool CheckAccessCore(OperationContext operationContext)
        {
            ApplicationConfiguration.Initialize(Sinacor.Infra.Common.Configuration.SystemComponents.System, Sinacor.Infra.Common.Configuration.SystemComponents.Service);

            //Verifica se o tipo de binding é um Mex, caso seja, retorna true para permitir a geração do proxy.
            if (operationContext.EndpointDispatcher.ChannelDispatcher.BindingName.IndexOf("Metadata") >= 0)
                return true;

            //Busca o nome do namespace do contrato.
            string contractNamespace = operationContext.EndpointDispatcher.ContractNamespace;
            //Busca o nome do contrato
            string contractName = operationContext.EndpointDispatcher.ContractName;
            //Busca o nome da operação
            string action = operationContext.RequestContext.RequestMessage.Headers.Action;

            //Monta o nome todo da ação
            string actionFullName = contractNamespace + "/" + contractName + "/" + action;

            //Cria o contexto de autorização
            AuthorizationContext authorizationContext = CreateAuthorizationContext(operationContext);

            //Se existir uma sessão, efetua a validação da mesma.
            //if (authorizationContext.SessionId.HasValue)
            //{

                //SessionControl sc = new SessionControl();
                //AccessControl ac = new AccessControl();
                
                //Se a sessão não for válida, bloqueia o acesso.
                //if (!sc.Validate(authorizationContext.SessionId.Value))
                    //throw new InvalidSessionException("Invalid session");

                ////Tenta renovar a sessão, caso a sessão não exista mais, bloqueia o acesso.
                //if (!sc.Renew(authorizationContext.SessionId.Value))
                    //throw new InvalidSessionException("Session does not exist");

                //Define o HostName do usuário na sessão criada.
                //sc.SetHostName(authorizationContext.SessionId.Value, authorizationContext.ClientHost.Ip);

                //Valida o acesso da operacao e sua respectiva licença de usuo
                if (ValidateOperationAccessAndLicense(authorizationContext.CompanyId, authorizationContext.ModuleId, actionFullName))
                    return true;
            //}

            //Retorna uma exceção de AccessDenied caso nenhuma das condições tenha sido validadas.
            throw new AccessIsDeniedException(string.Format("Access is denied for action '{0}'", actionFullName));
        }


        /// <summary>
        /// Cria o contexto de autorização baseado 
        /// </summary>
        /// <param name="operationContext"></param>
        /// <returns></returns>
        private AuthorizationContext CreateAuthorizationContext(OperationContext operationContext)
        {
            AuthorizationContext authorizationContext = new AuthorizationContext();

            //Recupera as informações do Header da Mensagem.
            authorizationContext = CreateAuthorizationContextFromHeader();

            if (authorizationContext == null)
            {

                authorizationContext = new AuthorizationContext();

                //Varre os claimSets do contexto de Operação
                foreach (ClaimSet cs in operationContext.ServiceSecurityContext.AuthorizationContext.ClaimSets)
                {
                    foreach (Claim c in cs.FindClaims(SinacorClaimTypes.Identity.SessionId, Rights.PossessProperty))
                        authorizationContext.SessionId = int.Parse(c.Resource.ToString());

                    foreach (Claim c in cs.FindClaims(SinacorClaimTypes.Identity.UserId, Rights.PossessProperty))
                        authorizationContext.UserId = int.Parse(c.Resource.ToString());

                    foreach (Claim c in cs.FindClaims(SinacorClaimTypes.Identity.CompanyId, Rights.PossessProperty))
                        authorizationContext.CompanyId = int.Parse(c.Resource.ToString());

                    foreach (Claim c in cs.FindClaims(SinacorClaimTypes.Identity.LoginName, Rights.PossessProperty))
                        authorizationContext.LoginName = c.Resource.ToString();

                    foreach (Claim c in cs.FindClaims(SinacorClaimTypes.Identity.Password, Rights.PossessProperty))
                        authorizationContext.Password = c.Resource.ToString();

                    foreach (Claim c in cs.FindClaims(SinacorClaimTypes.Identity.TokenId, Rights.PossessProperty))
                        authorizationContext.TokenId = c.Resource.ToString();

                    foreach (Claim c in cs.FindClaims(SinacorClaimTypes.Identity.SessionExpires, Rights.PossessProperty))
                        authorizationContext.SessionExpires = bool.Parse(c.Resource.ToString());
                }

                //Adiciona as informações do ClientHost no contexto de autorização.
                authorizationContext.ClientHost = RetrieveClientHostInformation();
            }

            //Busca o Id do Modulo configurado no arquivo de configuração
            authorizationContext.ModuleId = System.Configuration.ConfigurationManager.AppSettings["ModuleId"];

            //Se existir uma empresa no contexto, recupera os dados da empresa.
            if (authorizationContext.CompanyId != 0)
            {
                AccessControl ac = new AccessControl();
                Company company = ac.RetrieveCompany(authorizationContext.CompanyId);

                authorizationContext.CompanyName = company.Name;
                authorizationContext.CompanyCGC = company.CGC;
            }


            //Define o authorizationContext corrente;
            AuthorizationContext.Current = authorizationContext;

            return authorizationContext;
        }

        /// <summary>
        /// Recupera as informações do Contexto de Authorização do Header da Mensagem;
        /// </summary>
        /// <returns></returns>
        private AuthorizationContext CreateAuthorizationContextFromHeader()
        {
            AuthorizationContext authorizationContext = null;

            OperationContext operation = OperationContext.Current;

            SinacorUserInfo userInfo = null;
            ClientHost clientHost = null;

            if (operation != null)
            {
                MessageHeaders headers = operation.IncomingMessageHeaders;
                int headerIndex = headers.FindHeader(AuthenticationHeader.Name, AuthenticationHeader.NameSpace);

                if (headerIndex >= 0)
                {
                    AuthenticationHeader header = headers.GetHeader<AuthenticationHeader>(headerIndex);
                    if (header != null && header.UserInfo != null && header.UserInfo.UserId != 0)
                    {
                            authorizationContext = new AuthorizationContext();
                            userInfo = header.UserInfo;
                            clientHost = new ClientHost(header.ClientHostName, header.ClientIp);

                            authorizationContext.UserId = userInfo.UserId;
                            authorizationContext.SessionExpires = userInfo.SessionExpires;
                            authorizationContext.LoginName = userInfo.UserName;
                            authorizationContext.Password = userInfo.Password;
                            authorizationContext.CompanyId = userInfo.CompanyId;
                            authorizationContext.SessionId = userInfo.SessionId;
                            authorizationContext.TokenId = userInfo.TokenId;
                            authorizationContext.ClientHost = clientHost;
                    }
                }
            }

            return authorizationContext;
        }

        /// <summary>
        //Valida o acesso da operacao e sua respectiva licença de uso.
        /// </summary>
        /// <returns></returns>
        private bool ValidateOperationAccessAndLicense(int companyId, string moduleId, string actionFullName)
        {
            IAuthorizationProvider authorizationProvider;
            LicensingManager licensingManager;
            SHA384Managed Alg;
            bool isLicensed = false;            
            bool throwError = true;
            string computedOperationHash;
            Operation operation = null;

            //Se a operação corrente for do serviço licença permitir o acesso
            if (actionFullName == Constants.SinacorOperationLincensingName)
                return true;

            //Recupera a instacia do LicensingManager
            licensingManager = LicensingManager.GetInstance();
            //Recupera a instacia do AuthorizationProvider
            authorizationProvider = AuthorizationProviderFactory.RetrieveAuthorizationProvider();

            //Verifica se a operação existe no Banco de dados e recupera o tipo e o hash desta operação
            operation = authorizationProvider.CheckAcessToOperation(companyId, actionFullName);

            //Se a condição testada for verdadeira então a operação não existe no banco de dados portanto retornar falso
            if (operation == null)
                return false;

            try
            {
                //Recupera o arquivo de licença localizado no servidor
                _mutex.WaitOne();
                
                if (licensingManager != null && licensingManager.EncryptedLicensing == null)
                    licensingManager.EncryptedLicensing = GetLicesingFromServer();
            }
            catch
            {
                throw;
            }
            finally 
            {
                _mutex.ReleaseMutex();
            }

            //Somente valida a licença se o módulo não for de INFRA
            if (moduleId.ToUpper() != "INFRA" && companyId != 0)
            {
                //Recupera o dados da empresa
                Company company = new Company() { Id = AuthorizationContext.Current.CompanyId, Name = AuthorizationContext.Current.CompanyName, CGC = AuthorizationContext.Current.CompanyCGC };

                foreach (string module in operation.AllowedModules)
                {
                    //Se o client for Sinacor validar somente a licença do módulo, caso contrario validar modulo mais e kernel mais.
                    if (ValidateSinacorClientToken())
                        isLicensed = licensingManager.ValidateModule(company, module, false);
                    else
                        isLicensed = licensingManager.ValidateModule(company, module, true) && licensingManager.ValidateKernelPlus(company);

                    if (isLicensed)
                        break;
                }
            }
            else
                isLicensed = true;

            if (!isLicensed)
            {
                //Gera o hash a partir do nome e tipo da operação e compara com o retorno do banco para garantir a autenticidade da informação
                //O motivo de estar utilizando a classe System.Security.Cryptography.SHA384Managed diretamento e não a classe 
                //Sinacor.Infra.Common.Security.Cryptography.HashCryptographer é performace
                Alg = new SHA384Managed();
                computedOperationHash = Convert.ToBase64String(Alg.ComputeHash(Encoding.ASCII.GetBytes(operation.Name + "/" + operation.Type)));

                //Compara o hash gerado com o recebido do banco
                if (operation.Hash == computedOperationHash)
                    //Somente impedir o acesso a operação se esta não for do tipo Consulta
                    throwError = (operation.Type != SinacorClaimTypes.OperationType.Read);

                if (throwError)
                    throw new LicensingException();
                    //throw new LicensingException(licensingManager.Modules);
            }

            return true;
        }

        /// <summary>
        /// Verifica se o client é um Sinacor Application
        /// </summary>
        /// <returns>true or false</returns>
        private bool ValidateSinacorClientToken()
        {
            OperationContext operation = OperationContext.Current;
            string clientToken = string.Empty;

            if (operation != null)
            {
                MessageHeaders headers = operation.IncomingMessageHeaders;
                int headerIndex = headers.FindHeader(AuthenticationHeader.Name, AuthenticationHeader.NameSpace);

                if (headerIndex >= 0)
                {
                    AuthenticationHeader header = headers.GetHeader<AuthenticationHeader>(headerIndex);
                    if (header != null)
                        clientToken = header.SinacorClientToken;
                }
            }

            return clientToken == Constants.SinacorClientToken;
        }

        /// <summary>
        /// Recupera as informações da Mensagem, referentes ao ClientHost
        /// </summary>
        /// <returns></returns>
        private ClientHost RetrieveClientHostInformation()
        {
            ClientHost clientHost = null;

            OperationContext operation = OperationContext.Current;

            if (operation != null)
            {
                MessageHeaders headers = operation.IncomingMessageHeaders;
                int headerIndex = headers.FindHeader(AuthenticationHeader.Name, AuthenticationHeader.NameSpace);

                if (headerIndex >= 0)
                {
                    AuthenticationHeader header = headers.GetHeader<AuthenticationHeader>(headerIndex);
                    if (header != null)
                        clientHost = new ClientHost(header.ClientHostName, header.ClientIp);
                }
            }

            return clientHost;
        }

        /// <summary>
        /// Recupera o arquivo de licença localizado no servidor
        /// </summary>
        /// <returns></returns>
        private byte[] GetLicesingFromServer()
        {
            ILicensingProvider licensingProvider = AuthorizationProviderFactory.RetrieveLicensingProvider();
            return licensingProvider.GetLicesingFromServer();
        }
    }
}
